Algorytmy dopasowywania wzroców do tekstu

dr inż. Aleksander Smywiński-Pohl

konsultacje: śr. 15:00 - 16:00

Plan

  • algorytm naiwny
  • automat skończony
  • algorytm Knutha-Morrisa-Pratta

Oznaczenia

  • $T[1..n]$ - tekst o długości $n$, tablica znaków, łańcuch znaków
  • $P[1..m]$ - wzorzec o długości $m$, $m \leq n$
  • $\Sigma$ - alfabet, zbiór znaków tekstu
  • $\Sigma^{*}$ - zbiór wszystkich łańcuchów skończonej długości nad alfabetem $\Sigma$
  • $\epsilon$ - łańcuch pusty
  • $|x|$ - długość łańcucha $x$
  • $xy$ - konkatenacja łańcuchów $x$ i $y$

Definicje

  • $w \sqsubset x$ - $w$ jest prefiksem łańcucha $x$ $\equiv \exists y \in \Sigma^*: x = wy$
    • $|w| \leq |x|$
  • $w \sqsupset x$ - $w$ jest sufiksem łańcucha $x$ $\equiv \exists y \in \Sigma^*: x = yw$
    • $|w| \leq |x|$
  • $P_k$ - prefiks długości $k$ wzorca/tekstu $P$
  • wzorzec $P$ występuje w tekście $T$ z przesunięciem $s$ $\equiv$ $0 \leq s \leq n - m \land T[s+1..s+m] = P[1..m]$
  • poprawne przesunięcie $s$ - wzorzec $P$ występuje z przesunięciem $s$
  • niepoprawne przesunięcie $s$ - wzorczec $P$ nie występuje z przesunięciem $s$
  • dopasowanie wzroca $P$ do tekstu $T$
    • znalezienie wszystkich poprawnych przesunięć $s$ wzroca $P$ w tekście $T$
    • znalezienie wszystkich przesunięć $s$, dla których $P \sqsupset T_{s+m}$

Złożoność obliczeniowa algorytmów dopasowywania wzorców

 

Algorytm Czas preprocessingu Czas dopasowywania
naiwny 0 $O((n-m+1)m)$
Rabin-Karp $\Theta(m)$ $O((n-m+1)m$
automat skończony $O(m \Sigma )$ $\Theta(n)$
Knuth-Morris-Pratt $\Theta(m)$ $\Theta(n)$

Lemat 1.1 (o zawieraniu sufiksów)

$x, y, z$ - łańcuchy znaków

$x \sqsupset z \land y \sqsupset z \rightarrow$

  • $|x| \leq |y| \rightarrow x \sqsupset y$
  • $|x| \geq |y| \rightarrow y \sqsupset x$
  • $|x| = |y| \rightarrow x = y$

Algorytm naiwny

In [97]:
def naive_string_matching(text, pattern):
    for s in range(0, len(text) - len(pattern) + 1):
        if(pattern == text[s:s+len(pattern)]):
            print(f"Przesunięcie {s} jest poprawne")
In [98]:
naive_string_matching("abaabaaaaba", "aba")
Przesunięcie 0 jest poprawne
Przesunięcie 3 jest poprawne
Przesunięcie 8 jest poprawne

Złożoność czasowa dopasowania $O((n - m + 1)m)$

Automat skończony - definicja

Automat skończony $M$ to krotka $(Q, q_0, A, \Sigma, \delta)$:

  • $Q$ - zbiór stanów
  • $q_0 \in Q$ - stan początkowy
  • $A \subset Q$ - zbiór stanów akceptujących
  • $\Sigma$ - alfabet, skończony zbiór znaków
  • $\delta: Q \times \Sigma \rightarrow Q$ - funkcja przejścia
  • automat akceptuje łańcuch $\equiv$ stan po przeczytaniu łańcucha $q \in A$
  • w przeciwnym razie automat odrzuca łańcuch
  • automat indukuje funkcję $\phi(w)$ zwaną funkcją stanu końcowego
  • $\phi(w): \Sigma^* \rightarrow Q$
    • $\phi(\epsilon) = q_0$
    • $\phi(wa) = \delta(\phi(w), a)$ dla $w \in \Sigma^*, a \in \Sigma$

Funkcja sufiksowa $\sigma$ odpowiadająca wzorcowi $P$

$|P| = m$

$\sigma: \Sigma^* \rightarrow \{0, 1, ...m\}$

  • $\sigma(x) = max\{k: P_k \sqsupset x\}$
  • $P_0 = \epsilon$ jest sufiksem każdego łańcucha, co zapewnio poprawność definicji funkcji
  • $|P| = m \rightarrow (\sigma(x) = m \leftrightarrow P \sqsupset x)$

Automat akceptujący teksty pasujące do wzorca $P[1..m]$

  • $Q = \{0, 1, ..., m\}$
  • $q_0 = 0$
  • $A = \{m\}$ - jedyny stan akceptujący
  • $\pmb{\delta(q, a) = \sigma(P_{q}a)}$

Jak skonstruować funkcję $\delta$? Kluczowa obserwacja:

$\pmb{\sigma(T_ia) = \sigma(P_qa)}$, gdzie $q = \sigma(T_i)$

Innymi słowy - można to zrobić analizująca sam wzorzec $P$.

Algorytm automatu skończonego

In [100]:
def fa_string_matching(text, delta):
    q = 0
    for s in range(0, len(text)):
        q = delta[q][text[s]]
        if(q == len(delta) - 1):
            print(f"Przesunięcie {s + 1 - q} jest poprawne")
            # s + 1 - ponieważ przeczytaliśmy znak o indeksie s, więc przesunięcie jest po tym znaku

Złożoność czasowa dopasowania $\Theta(n)$

Funkcja przejścia

In [99]:
pattern = "aba"

delta = [
    {"a": 1, "b": 0}, # 0
    {"a": 1, "b": 2}, # 1
    {"a": 3, "b": 0}, # 2
    {"a": 1, "b": 2}, # 3
]
In [101]:
fa_string_matching("abaabaaaaba", delta)
Przesunięcie 0 jest poprawne
Przesunięcie 3 jest poprawne
Przesunięcie 8 jest poprawne

Poprawność działania automatu skończonego

  • lemat o nierówności funkcji sufiksowej $\sigma$
  • lemat o rekursywności funkcji sufiksowej $\sigma$
  • twierdzenie o porawności automatu skończonego

Lemat 1.2 (o nierówności funkcji sufiksowej)

Przyjmując:

  • $x$ - łańcuch znaków
  • $a$ - znak

$\pmb{\forall x, a: \sigma(xa) \leq \sigma(x) + 1}$

  • Niech $r = \sigma(xa)$

  • Jeśli $r = 0$, wtedy warunek nierówności jest spełniony, ponieważ ponieważ $\sigma$ jest nieujemna.

  • Jeśli $r > 0$:
    • $P_r \sqsupset xa$ z definicji $\sigma$
    • $P_{r-1} \sqsupset x$ poprzez usunięcie $a$
    • $r-1 \leq \sigma(x)$ ponieważ $\sigma(x)$ jest największym $k$ takim, że $P_k \sqsupset x$
    • $\sigma(xa) = r \leq \sigma(x) + 1$

Lemat 1.3 (o rekursywności funkcji sufiksowej)

Przyjmując:

  • $x$ - łańcuch znaków
  • $a$ - znak

$\pmb{\forall x, a: q = \sigma(x) \rightarrow \sigma(xa) = \sigma(P_qa)}$

  • $P_q \sqsupset x$ z definicji $\sigma$
  • $P_qa \sqsupset xa$ poprzez dodanie znaku $a$ na końcu prefiksu wzorca oraz na końcu tekstu
  • niech $r = \sigma(xa)$, wtedy:
    • $P_r \sqsupset xa$
    • $r \leq q + 1$ z lematu 1.2
    • $|P_r| = r \leq q+1 = |P_qa|$
    • $\pmb{P_r \sqsupset P_qa}$
      • z $P_qa \sqsupset xa$, $P_r \sqsupset xa$ oraz $|P_r| \leq |P_qa|$ i lematu 1.1
    • $r \leq \sigma(P_qa)$
    • $\sigma(xa) \leq \sigma(P_qa)$
    • $\sigma(P_qa) \leq \sigma(xa)$ ponieważ $P_qa \sqsupset xa$
    • $\sigma(xa) = \sigma(P_qa)$

Twierdzenie 1.4 (o poprawności automatu skończonego)

Niech:

  • $\phi$ - funkcja przejścia automatu skończonego, dla wzroca $P$,
  • $T[1..n]$ - tekst dopasowywany do wzorca $P$,

wtedy

$\pmb{\phi(T_i) = \sigma(T_i)}$

Dowód indukcyjny.

dla $i = 0$, twierdzenie jest prawdziwe, ponieważ $T_0 = \epsilon$, więc $\phi(T_0) = 0 = \sigma(T_0)$,

Załóżmy, że $\phi(T_i) = \sigma(T_i)$ i sprawdźmy, czy $\phi(T_{i+1}) = \sigma(T_{i+1})$.

Niech $q \equiv \phi(T_i)$ oraz $a \equiv T[i+1]$.

$\begin{array}{ccll} \phi(T_{i+1}) & = & \phi(T_ia) & \textrm{z definicji $T_{i+1}$ oraz $a$}\\ & = & \delta(\phi(T_i),a) & \textrm{z definicji $\phi$}\\ & = & \delta(q,a) & \textrm{z defnicji $q$}\\ & = & \sigma(P_qa) & \textrm{z definicji $\delta$}\\ & = & \sigma(T_ia) & \textrm{z lematu 1.3 oraz indukcji}\\ & = & \sigma(T_{i+1}) & \textrm{z definicji $T_{i+1}$} \end{array} $

Algorytm generujący funkcję przejścia

In [ ]:
import re

def transition_table(pattern):
    result = []
    for q in range(0, len(pattern) + 1):
        result.append({})
        for a in ["a", "b"]:
            k = min(len(pattern) + 1, q + 2)
            while True:
                k = k - 1
                if(re.search(f"{pattern[:k]}$", pattern[:q] + a)):
                    break
            result[q][a] = k    
    return result
In [ ]:
transition_table("aba")

Złożoność czasowa algorytmu generującego funkcję przejścia

$O(m^3|\Sigma|)$

Algorytm Knutha-Morrisa-Pratta

In [ ]:
def kmp_string_matching(text, pattern):
    pi = prefix_function(pattern)
    q = 0
    for i in range(0, len(text)):
        while(q > 0 and pattern[q] != text[i]):
            q = pi[q-1]
        if(pattern[q] == text[i]):
            q = q + 1
        if(q == len(pattern)):
            print(f"Przesunięcie {i + 1 - q} jest poprawne")
            q = pi[q-1]
In [ ]:
def prefix_function(pattern):
    pi = [0]
    k = 0
    for q in range(1, len(pattern)):
        while(k > 0 and pattern[k] != pattern[q]):
            k = pi[k-1]
        if(pattern[k] == pattern[q]):
            k = k + 1
        pi.append(k)
    return pi
In [ ]:
prefix_function("ababaca")
In [ ]:
kmp_string_matching("abaabaaaaba", "aba")